package com.me.Utils;

import com.me.annotation.Inject;

import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class DIContainer {
	private Map<Class, Class> scopes = new HashMap<>();
	private Map<Class, Object> instances = new HashMap<>();

	public DIContainer() {
		instances.put(DIContainer.class, this);
	}

	public void inject(Object injectable) {
		if (injectable == null) {
			return;
		}
		injectField(injectable);
		injectMethod(injectable);
	}

	private void injectField(Object injectable) {
		Class injectableClass = injectable.getClass();
		for (Field field : injectableClass.getDeclaredFields()) {
			try {
				if (field.getAnnotation(Inject.class) != null) {
					boolean acc = field.isAccessible();
					field.setAccessible(true);
					Object value = instances.get(field.getType());
					if (value == null) {
						value = resolve(scopes.get(field.getType()));
					}
					if (value != null) {
						field.set(injectable, value);
					}
					field.setAccessible(acc);
				}
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}
	}

	private void injectMethod(Object injectable) {
		Class injectableClass = injectable.getClass();
		for (Method method : injectableClass.getDeclaredMethods()) {
			if (method.getAnnotation(Inject.class) != null) {
				boolean acc = method.isAccessible();
				method.setAccessible(true);
				Class[] classes = method.getParameterTypes();
				Object[] objs = new Object[classes.length];
				for (int i = 0; i < classes.length; i++) {
					if (instances.get(classes[i]) != null) {
						objs[i] = instances.get(classes[i]);
					} else {
						objs[i] = resolve(scopes.get(classes[i]));
					}
				}
				invoke(injectable, method, objs);
				method.setAccessible(acc);
			}
		}
	}

	private void invoke(Object instance, Method method, Object[] args) {
		try {
			method.invoke(instance,args);
		} catch (IllegalAccessException | InvocationTargetException e) {
			e.printStackTrace();
		}

	}

	private Object create(Constructor ctor, Object[] params) {
		try {
			return ctor.newInstance(params);
		} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
			e.printStackTrace();
		}
		return null;
	}

	private Object create(Class type) {
		try {
			return type.getDeclaredConstructor().newInstance();
		} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
			e.printStackTrace();
		}
		return null;
	}

	public void bind(Class type, Object value) {
		instances.put(type, value);
	}

	public void bind(Class type,Class impl) {
		scopes.put(type, impl);
	}

	public void bindInstance(Object instance) {
		instances.put(instance.getClass(), instance);
	}

	public Object resolve(Class type) {
		if (type == null) {
			return null;
		}
		Constructor[] ctors = type.getDeclaredConstructors();
		for (Constructor ctor : ctors) {
			if (ctor.getAnnotation(Inject.class) != null) {
				boolean acc = ctor.isAccessible();
				ctor.setAccessible(true);
				Parameter[] params = ctor.getParameters();
				Object[] objs = new Object[params.length];
				for (int i = 0; i < params.length; i++) {
					objs[i] = instances.get(params[i].getType());
					if (objs[i] == null) {
						objs[i] = resolve(scopes.get(params[i].getType()));
					}
				}

				Object instance = create(ctor, objs);
				inject(instance);

				ctor.setAccessible(acc);
				return instance;
			}
		}

		Object o = instances.get(type);
		o = o == null ? create(type) : o;
		inject(o);
		return o;
	}
}